Control Tower環境下でSecurityHubのセキュリティ基準・コントロール無効化を自動化する
Control Tower環境でSecurity HubのOrganizations連携の自動有効化を利用した際、デフォルトで有効化されるCIS AWS Foundations BenchmarkとAWS 基礎セキュリティのベストプラクティスの一部コントロールを無効化したいケースがありました。これらをControl Towerのカスタマイズソリューション(以下CfCT)を活用して既存アカウント・新規アカウントの自動化を実装してみます。
どのように実装するのか?
Security HubのコントロールはOrganizations連携をしている管理者側で個別に無効化をしても、メンバーアカウント側には反映されません。 無効化する方法としてはAWS CLIかAPI、コンソールとなるのですが、自動化するには少し工夫が必要です。
個々のコントロールの無効化と有効化 - AWS Security Hub
今回はCfCTを使ってCloudFormationスタックからLambda-backedカスタムリソースを実行し、セキュリティ基準の無効化とコントロールの無効化を実現しています。(Management Accountにあるリソースは全てCfCTのソリューションを展開すれば作成されます。)
これらを実装することで、それぞれ以下の順序で自動化が実現できます。
既存アカウントへデプロイする場合
- マネジメントアカウントにCfCTを展開
- マニフェストファイル・CFnテンプレートを作成
- CfCT展開時に作成されたCodeCommitへPush
- CodeCommitへのPushからCfCTのCodePipelineが自動起動
- CfCT上のStackSetsから自動で対象のアカウントへスタックを作成
- カスタムリソースからSecurity Hubのセキュリティ基準・コントロールを無効化
1.2.3を実施すれば、4.5.6はCfCTで自動化されるのでやることはありませんが、フローを理解するために書いておきます。
新規アカウント発行時
- Control Towerからアカウントを発行
- Organizations連携から新規アカウントのOrganizationsを有効化
- 新規アカウント発行のライフサイクルイベントからCfCTのCodePipelineが自動起動
- CfCT上のStackSetsから自動で対象のアカウントへスタックを作成
- カスタムリソースからSecurity Hubのセキュリティ基準・コントロールを無効化
こちらはCfCTが展開されていて、マニフェストファイルとCFnテンプレートがCodeCommitにPushされていることが前提です。既存アカウントへのデプロイを行っていれば、特に追加の実装は必要ありません。
前提
- Landing Zone バージョン2.7
- Security HubのOrganizations連携済
- Control Towerのカスタマイズ展開済
- CodeCommitで展開されたリポジトリをクローン
Security HubのOrganizations連携
まだSecurity HubのOrganizations連携が済んでいない場合は以下の記事を参考にしてください。今回は東京リージョンだけを対象に自動有効化して実施しています。
全リージョンを対象としたい場合はこちらをご覧ください。
CfCTの展開
CfCTというソリューションを利用して自動化しますので、以下の記事を参考にソリューションの展開を実施してください。
以降の説明では、ソリューションがマネジメントアカウント上に展開されている前提で進めていきます。ソースはS3を選択することもできますが、CodeCommitで進めています。
マニフェストファイルの作成
StackSetsを展開するための定義情報としてマニフェストファイルを作成します。Control Towerのカスタマイズからクローンして初期ファイルは全て削除してください。その中にmanifest.yaml
を作成します。
custom-control-tower-configuration ├── manifest.yaml
マニフェストファイルの詳細な記述方法については開発者ガイドをご参照ください。今回は以下のように作成しています。
--- #Default region for deploying Custom Control Tower: Code Pipeline, Step functions, Lambda, SSM parameters, and StackSets region: ap-northeast-1 # Control Tower Home Region version: 2021-03-15 resources: # Control Tower Custom CloudFormation Resources - Disable SecurityHub Subscription - name: update-securityhub-subscription description: Control Tower Custom CloudFormation Resources - Disable SecurityHub Subscription resource_file: template/DisableSecurityHubSubscription.yaml deploy_method: stack_set deployment_targets: organizational_units: - Sandbox parameters: - parameter_key: DisableList parameter_value: IAM.1,IAM.2,IAM.3 # - parameter_key: EnableList # parameter_value: EC2.8,IAM.6,S3.5,CloudTrail.2 regions: - ap-northeast-1
resource_fileで指定しているtemplate/DisableSecurityHubControl.yaml
のファイルは後で作成します。今回展開対象のOUはSandboxとしています。Sandboxにはテスト用のアカウントAが1つ有ります。
parametersについて
parametersセクションでは、後続のCloudFormationでコントロールを無効化する際に必要なIDを指定しています。parameter_value: IAM.1,IAM.2,IAM.3
とカンマ区切りでリストを作成していて、CloudFormationへのインプットとなります。
parameter_key: EnableList
の部分はコメントアウトしていますが、一度無効化したコントロールを再有効化したい場合にコメントアウトを外して利用してください。後ほど利用する方法も合わせて紹介します。
CloudFormationテンプレートの作成
新規アカウントにデプロイするためのCloudFormationテンプレートを作成します。templateフォルダを作成して、その中にUpdateSecurityHubControl.yaml
としてテンプレートを作成することで以下のようなフォルダ構成になります。
custom-control-tower-configuration ├── manifest.yaml └── template └── UpdateSecurityHubControl.yaml
作成するテンプレートは以下の通りです。カスタムリソースを利用しているため非常にコードが長くなっています。利用される方はクリックして展開してください。
UpdateSecurityHubControl.yaml(クリックすると展開されます)
AWSTemplateFormatVersion: "2010-09-09" Parameters: DisableList: Type: CommaDelimitedList Default: "" EnableList: Type: CommaDelimitedList Default: "" Conditions: IsDisableListCondition: Fn::Not: [!Equals [!Join [",", !Ref DisableList], ""]] IsEnableListCondition: Fn::Not: [!Equals [!Join [",", !Ref EnableList], ""]] Resources: DisableSecurityHubLambda: Type: Custom::DisableSecurityHubLambda Condition: IsDisableListCondition Properties: ServiceToken: !GetAtt "DisableLambdaFunction.Arn" DisableList: !Ref DisableList EnableSecurityHubLambda: Type: Custom::DisableSecurityHubLambda Condition: IsEnableListCondition Properties: ServiceToken: !GetAtt "EnableLambdaFunction.Arn" EnableList: !Ref EnableList DisableLambdaFunction: Type: AWS::Lambda::Function Properties: Role: !GetAtt "LambdaExecutionRole.Arn" Runtime: "python3.8" Handler: index.lambda_handler Timeout: "180" Code: ZipFile: | import sys import boto3 import cfnresponse from logging import getLogger, INFO logger = getLogger() logger.setLevel(INFO) def disable_securityhub(event, context): # params logger.info('[START] disable_securityhub') standard_name = "cis-aws-foundations-benchmark/v/1.2.0" disable_list = event['ResourceProperties']['DisableList'] disable_controls = {"aws-foundational-security-best-practices/v/1.0.0": disable_list} target_regions = ["us-east-1", "ap-northeast-1", "ap-southeast-1"] disable_reason = "init disable" try: account_id = boto3.client('sts').get_caller_identity()['Account'] ec2_client = boto3.client('ec2') regions = ec2_client.describe_regions()['Regions'] for region in regions: region_name = region['RegionName'] logger.info("# " + region_name) if region_name not in target_regions: logger.info("skip region.") continue securityhub = boto3.client('securityhub', region_name=region_name) # check enable security hub try: securityhub.get_enabled_standards() except securityhub.exceptions.InvalidAccessException as e: logger.info("Security Hub is disabled.") continue # disable standard std_subsc_arn = "arn:aws:securityhub:{}:{}:subscription/{}".format( region_name, account_id, standard_name) res = securityhub.batch_disable_standards( StandardsSubscriptionArns=[std_subsc_arn]) if res['ResponseMetadata']['HTTPStatusCode'] == 200: logger.info('Disable Standard Success.') else: logger.info('Disable Standard Failed.') # disable control for standard in disable_controls: logger.info('Disable Controls in ' + standard) for control in disable_controls[standard]: logger.info(' Target Control: ' + control) ctl_arn = "arn:aws:securityhub:{}:{}:control/{}/{}".format( region_name, account_id, standard, control) res = securityhub.update_standards_control( StandardsControlArn=ctl_arn, ControlStatus='DISABLED', DisabledReason=disable_reason) if res['ResponseMetadata']['HTTPStatusCode'] == 200: logger.info(' Disable Success.') else: logger.info(' Disable Failed.') logger.info('[END] disable_securityhub') except Exception as e: logger.error(e) cfnresponse.send(event, context, cfnresponse.FAILED, {'Response': 'Failure'}) exit() def lambda_handler(event, context): logger.info('[START] lambda_handler') if event['RequestType'] == 'Create': disable_securityhub(event, context) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) if event['RequestType'] == 'Delete': cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) if event['RequestType'] == 'Update': disable_securityhub(event, context) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) logger.info('[END] lambda_handler') EnableLambdaFunction: Type: AWS::Lambda::Function Properties: Role: !GetAtt "LambdaExecutionRole.Arn" Runtime: "python3.8" Handler: index.lambda_handler Timeout: "180" Code: ZipFile: | import sys import boto3 import cfnresponse from logging import getLogger, INFO logger = getLogger() logger.setLevel(INFO) def enable_securityhub(event, context): # params logger.info('[START] enable_securityhub_control') enable_list = event['ResourceProperties']['EnableList'] enable_controls = {"aws-foundational-security-best-practices/v/1.0.0": enable_list} target_regions = ["ap-northeast-1"] try: account_id = boto3.client('sts').get_caller_identity()['Account'] ec2_client = boto3.client('ec2') regions = ec2_client.describe_regions()['Regions'] for region in regions: region_name = region['RegionName'] logger.info("# " + region_name) if region_name not in target_regions: logger.info("skip region.") continue securityhub = boto3.client('securityhub', region_name=region_name) # enable control for standard in enable_controls: logger.info('Enable Controls in ' + standard) for control in enable_controls[standard]: logger.info(' Target Control: ' + control) ctl_arn = "arn:aws:securityhub:{}:{}:control/{}/{}".format( region_name, account_id, standard, control) res = securityhub.update_standards_control( StandardsControlArn=ctl_arn, ControlStatus='ENABLED') if res['ResponseMetadata']['HTTPStatusCode'] == 200: logger.info(' Enable Success.') else: logger.info(' Enable Failed.') logger.info('[END] enable_securityhub_control') except Exception as e: logger.error(e) cfnresponse.send(event, context, cfnresponse.FAILED, {'Response': 'Failure'}) exit() def lambda_handler(event, context): logger.info('[START] lambda_handler') if event['RequestType'] == 'Create': enable_securityhub(event, context) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) if event['RequestType'] == 'Delete': cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) if event['RequestType'] == 'Update': enable_securityhub(event, context) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'}) logger.info('[END] lambda_handler') LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / Policies: - PolicyName: lambda-disable-securityhub-policy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: arn:aws:logs:*:*:* - Effect: Allow Action: - securityhub:GetEnabledStandards - securityhub:BatchDisableStandards - securityhub:UpdateStandardsControl - ec2:DescribeRegions - sts:GetCallerIdentity Resource: "*"
簡単に各項目について説明します。
DisableSecurityHubLambda
セキュリティ基準・コントロールを無効化するLambdaを起動するカスタムリソースです。無効化するコントロールのリストはマニフェストファイルから取得するパラメータであるDisableList
をLambdaに引き渡しています。
マニフェストファイルからのインプットがない場合は、Condition
を使ってリソース自体を作成しないようにしています。
EnableSecurityHubLambda
セキュリティ基準・コントロールを有効化するLambdaを起動するカスタムリソースです。無効化するコントロールのリストはマニフェストファイルから取得するパラメータであるEnableList
をLambdaに引き渡しています。
こちらもDisableSecurityHubLambda
と同様マニフェストファイルからのインプットがない場合は、Condition
を使ってリソース自体を作成しないようにしています。
DisableLambdaFunction
セキュリティ基準・コントロールを無効化するLambdaです。無効化するセキュリティ基準はCIS AWS Foundations Benchmarkを指定しています。コントロールの無効化対象はDisableSecurityHubLambda
から引き渡された値です。
実行する対象のリージョンは東京リージョンap-northeast-1
だけにしています。マニフェストファイル側で対象のリージョンを書くことで、その他のリージョンにStackSetsから展開することも可能なのですが、同じLambdaを複数のリージョンに作成するのは嫌だったのでこのような形にしています。もし無効化対象としたいリージョンを増やしたい場合は、Lambda内にあるtarget_regions
にリスト形式で追記してください。
EnableLambdaFunction
コントロールを有効化するLambdaです。コントロールの無効化対象はEnableSecurityHubLambda
から引き渡された値です。こちらも対象リージョンは東京だけになっているので、増やしたい場合は追加して下さい。
LambdaExecutionRole
Lambda用のロールです。SecurityHub周りとログ出力に必要な権限を追加しています。
既存アカウントへのデプロイ
それでは準備が完了したので、まずは既存アカウントへのデプロイを行います。
ここでデプロイされるアカウントはマニフェストファイルで定義したdeployment_targets
の部分です。
deployment_targets: organizational_units: - Sandbox
マニフェストファイルとテンプレートが用意できたら、CodeCommitにPushします。
$ git add -A $ git commit -m 'Disable SecurityHub Subscription' $ git push
Pushが問題なく完了すれば、Control Towerのカスタマイズで作成されているパイプラインが動き始めます。
展開するスタック数などによって時間は前後しますが、ここまで同じ内容でやっていれば15分程度かかります。最後のCloudformationResource
のフェーズが成功すれば展開は完了です。
今回の場合はマニフェストファイルで対象をSandboxのOUとしたので、アカウントAを確認してみます。
CIS AWS Foundations Benchmarkは無効化されていました。
AWS 基礎セキュリティのベストプラクティス内のマニフェストファイルで定義した「IAM.1,IAM.2,IAM.3」が無効の項目に入っていることが分かります。
StackSetsで展開されているので、CloudFormationのコンソールを確認すると、以下のようなスタックが作成されていることが分かります。
ここまでで、既存アカウントへのデプロイは完了です。
既存アカウントへの変更
一度無効化して終わり、ではなく無効化する対象を変更したり一度無効化したものを再有効化することも可能です。マニフェストファイルからparameters
を変更してみましょう。DisableListを「IAM.4,IAM.5,IAM.6」、EnableListを先ほど無効化した「IAM.1,IAM.2,IAM.3」を再有効化するよう以下のように変更します。
--- #Default region for deploying Custom Control Tower: Code Pipeline, Step functions, Lambda, SSM parameters, and StackSets region: ap-northeast-1 # Control Tower Home Region version: 2021-03-15 resources: # Control Tower Custom CloudFormation Resources - Disable SecurityHub Subscription - name: update-securityhub-subscription description: Control Tower Custom CloudFormation Resources - Disable SecurityHub Subscription resource_file: template/DisableSecurityHubSubscription.yaml deploy_method: stack_set deployment_targets: organizational_units: - Sandbox parameters: - parameter_key: DisableList parameter_value: IAM.4,IAM.5,IAM.6 - parameter_key: EnableList parameter_value: IAM.1,IAM.2,IAM.3 regions: - ap-northeast-1
変更が完了したら、同じようにCodeCommitへPushしてみましょう。CodePipelineの実行が完了したら、アカウントAを確認してみると再有効化の対象としてマニフェストファイルに指定した「IAM.1,IAM.2,IAM.3」が有効化されていることが確認できました。
無効のタブを確認してみると、無効化対象として定義した「IAM.4,IAM.5,IAM.6」について無効化されていることが確認できました。
ここまでで、既存アカウントへの変更は完了です。コントロールはマニフェストファイルから変更ができますが、無効化したものを有効化する際にはEnableList
への追加を忘れないようにしましょう。(DisableListから削除しただけでは有効化されず、無効化されたままになります。)
新規アカウントの発行
既存アカウントへの自動化ができたので、次は新規のアカウント発行時に自動化できているか確認してみます。CfCTの仕組みで、アカウントの新規発行時にControl TowerのライフサイクルイベントからCodePipelineが自動起動されるので、既存アカウントへのデプロイができていれば追加の実装は必要ありません。
早速新規のアカウントを発行して、Security Hubのコントロールが無効化されているのかを確認していきます。Control Towerのアカウントファクトリーからアカウントの登録を行います。
しばらくするとControl Towerから新規アカウントが登録済になります。その後CfCTのCodePioelineが自動で動き始めるので、完了するまで待ちましょう。アカウントの初期セットアップはControl TowerのベースラインやOrganizations連携も含まれるので、結構時間がかかります。(30分以上はかかると思います)
発行されたアカウントにログインしてみると、無効化したい「IAM.4,IAM.5,IAM.6」が無効タブに問題なく入っていました。
ということで、新規で作成されたアカウントでも既存アカウントと同様の設定を自動化できていることが確認できました。
まとめ
CfCTを使ったSecurity Hubのセキュリティ基準・コントロール無効化を自動化してみました。Control Tower環境で各アカウントのコントロール管理に困っている人の参考になれば幸いです。
CfCTのソリューションはこの用途以外にも様々なことが自動化できる良いソリューションなので、是非Control Towerを利用している人は導入を検討してみてください。